30. K-Means算法-股市波动聚类

本章概要

  • 学习材料:单一沪深300指数的半年窗口收益与波动特征。
  • 本章任务:依次运行 lst-ch30-local-featureslst-ch30-local-trainlst-ch30-local-check,按时间前70%拟合标准化器/K=2—5,再把后30%分配到固定质心。
  • 完成后你将得到:训练轮廓系数/最小簇占比、留出分配距离、漂移摘要和判断条件状态。
  • 自我检查:手算一个训练 z 分数并确认留出期未参与拟合;小簇、训练分离或留出样本距离检查未通过时 暂不采用模型
  • 拓展练习:把同一聚类结果判断拓展应用到长三角行业波动分组。

本节内容概览

  • K-Means算法是经典的无监督聚类方法
  • 核心思想:将数据自动分为K个组(簇)
  • 本节将其应用于股指波动数据的分类

什么是无监督学习?

  • 监督学习:数据有标签(如”涨”或”跌”)
  • 无监督学习:数据没有标签,让算法自行发现结构
  • K-Means 就是一种典型的无监督学习方法

K-Means算法的目标

最小化簇内平方和(SSE)

\[ \large SSE = \sum_{i=1}^{K}\sum_{x \in C_i} ||x - \mu_i||^2 \]

  • \(K\):簇的数量
  • \(C_i\):第 \(i\) 个簇中的所有数据点
  • \(\mu_i\):第 \(i\) 个簇的质心(均值中心)

K-Means算法四步流程

  1. 随机初始化 K 个质心
  2. 分配样本:将每个数据点归入最近的质心
  3. 更新质心:重新计算每个簇的均值
  4. 重复步骤 2-3,直到质心不再变化(收敛)

K-Means迭代过程示意

K-Means迭代四步流程 展示K-Means算法从初始化质心到收敛的四个步骤。 Step 1 随机初始化 K个质心 Step 2 分配每个样本 到最近质心 Step 3 重新计算 每个簇的质心 Step 4 收敛? 否则回到Step2 未收敛则重复迭代

如何确定K值?——肘部法则

  • 依次尝试 \(K=1,2,\dots,9\),计算每个K的SSE
  • 绘制SSE随K变化的折线图
  • 拐点(“肘部”)只是候选 K 的启发式线索;曲线平滑或出现多个弯折时,不能声称存在唯一最优值
  • 同时比较轮廓系数、重复抽样或跨期稳定性与业务可解释性;三者结论不一致或没有清晰分组时,不要急于给各组命名

运行前预测|平台任务:肘部法则与K-Means聚类

  • 输入预测:运行前先写出 dataSSEcolsestimator 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到label 的结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务:肘部法则与K-Means聚类”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务:肘部法则与K-Means聚类

展开完整代码(投影默认折叠)
# 注:case3.1.xlsx数据文件本地没有,但平台已经内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库
from sklearn.cluster import KMeans  # 导入Scikit-learn的KMeans模块
import matplotlib.pyplot as plt  # 导入Matplotlib绘图库
data = pd.read_excel("case3.1.xlsx")  # 从Excel文件读取数据存入data
SSE = []  # 定义列表SSE
cols = data.iloc[:, 2:]  # 提取数值型特征列(跳过前两列的非数值信息)
for k in range(1,10):  # 遍历range(1,10)中的每个k
  estimator = KMeans(n_clusters=k)  # 初始化K-Means聚类模型
    estimator.fit(cols)  # 在数据上训练estimator模型
    SSE.append(estimator.inertia_)  # 将当前K值的簇内误差平方和(SSE)添加到列表
plt.plot(range(1,10), SSE,'*-')  # 绘制折线图
plt.title('Elbow Method')  # 设置图表标题
plt.xlabel('K Clusters')  # 设置X轴标签
plt.ylabel('SSE')  # 设置Y轴标签
plt.savefig("1.png") #过程展示一
data1=data.drop('股指',axis=1)  # 删除指定行或列
clu = KMeans(n_clusters=3,random_state=30)  # 初始化K-Means聚类模型
clu.fit(data1)  # 在数据上训练clu模型
label = clu.labels_  # 获取聚类模型对每个样本的簇标签
print(label)# 结果展示

任务复盘|平台任务:肘部法则与K-Means聚类

平台状态 vs Python 状态:平台块必须原样录入,但标准 Python 中 estimator.fit 的缩进非法,不能称为可以在本地运行。以下外围纠错块才是合法 Python 结构;仍需标准化、固定随机种子并检查输入尺度。

拓展练习:把对象或期间改为一家长三角上市公司或一组 A 股资产;先预测指标方向,再说明结果能支持和不能支持的决策。

本地参考结构:先缩进纠错再验证

SSE = []  # 初始化候选簇数的簇内平方和
for cluster_count in range(1, 10):  # 逐个评估候选簇数
    estimator = KMeans(n_clusters=cluster_count, n_init=10, random_state=30)  # 固定初始化规则
    estimator.fit(cols)  # 在同一已核验特征矩阵上拟合
    SSE.append(estimator.inertia_)  # 记录每个候选的SSE

平台代码解读:肘部法则部分

  • data.iloc[:, 2:]:提取第3列起的所有数值特征
  • KMeans(n_clusters=k):创建K-Means模型
  • estimator.inertia_:获取当前K值下的SSE
  • 最终绘制SSE随K变化的折线图,找出拐点

平台代码解读:聚类执行部分

  • data.drop('股指', axis=1):删除非数值列
  • KMeans(n_clusters=3, random_state=30):设定3个簇
  • clu.labels_:获取每个股指的聚类标签
  • 输出标签数组,查看每个股指属于哪一类

真实数据验证:输入口径与预声明判断条件

平台数据 case3.1.xlsx 仅存在于教学平台;本课件用本地真实指数行情完成同一算法的可运行验证。

要素 课堂口径
本地资产 index/hs300_index_only.h5(沪深300日行情)
样本 2013H1—2025H2 的完整半年窗口(≥60个交易日)
特征 年化波动率、年化平均收益(对数收益口径)
方法 z标准化后 K-Means,K=2—5 按轮廓系数选优

真实数据验证:预声明判断条件

课堂判断标准:训练期轮廓系数 ≥ 0.30;训练期最小组占比 ≥ 15%;留出样本至少覆盖两个训练组,且至少 80% 的留出样本距离不超过训练样本距离的 95% 分位数。80% 只是课堂示例,还要同时比较 70% 和 90% 两种设置;任一标准不满足时,暂不采用聚类结果。

  • 通过:只允许描述该样本期内的固定质心分配与距离漂移。
  • 不符合标准时:说明“暂不采用模型”,不输出状态标签或配置建议。
  • 核对:更换 K 范围、窗口或特征后,重新运行全部三项判断条件。

真实数据验证:窗口特征构建

Listing 1
展开真实指数特征构建代码
from pathlib import Path  # 导入路径工具以探测课程数据挂载
import numpy as np  # 导入数值计算库
import pandas as pd  # 导入数据分析库
from sklearn.cluster import KMeans  # 导入K-Means聚类模型
from sklearn.metrics import silhouette_score  # 导入轮廓系数评估指标
candidate_roots = [Path('/home/ubuntu/r2_data_mount/data'), Path(r'C:\qiufei\data')]  # 按课程规定顺序探测数据根目录
data_root = next((root for root in candidate_roots if root.is_dir()), None)  # 选择第一个可用根目录
index_path = data_root / 'index' / 'hs300_index_only.h5' if data_root else None  # 定位沪深300本地行情文件
if index_path is None or not index_path.is_file():  # 真实数据缺失时明确提示
    raise FileNotFoundError(f'未找到沪深300数据文件:{index_path};请从课程数据下载入口获取')  # 提示读者下载文件
hs300_daily = pd.read_hdf(index_path, key='hs300')  # 读取真实指数日行情
trade_date = pd.to_datetime(hs300_daily['datetime'].astype(str).str[:8], format='%Y%m%d')  # 解析八位交易日字符串
daily_log_return = np.log(hs300_daily['close'] / hs300_daily['prev_close'])  # 用收盘与昨收计算对数收益
daily_log_return.index = trade_date  # 以交易日作为收益序列索引
window_sample = daily_log_return.sort_index().loc['2013-01-01':'2026-06-30'].dropna()  # 截取课堂窗口样本期
half_year_code = window_sample.index.year * 10 + (window_sample.index.month > 6).astype(int) + 1  # 为每个交易日标记所属半年
window_stats = window_sample.groupby(half_year_code).agg(['count', 'mean', 'std'])  # 聚合每个半年的交易日数/均值/标准差
minimum_window_days = 60  # 预声明的完整半年窗口最少交易日
window_stats = window_stats[window_stats['count'] >= minimum_window_days]  # 过滤残缺窗口(如仅约20个交易日的2026H1)
window_features = pd.DataFrame({'annualized_return': window_stats['mean'] * 252, 'annualized_volatility': window_stats['std'] * np.sqrt(252)})  # 构造年化特征
def format_half_year(code):  # 将半年编码转为可读标签
    return f'{code // 10}H{code % 10}'  # 前四位为年份,末位1或2为半序
print(f'输入口径:{len(window_features)}个完整半年窗口({format_half_year(window_features.index[0])}{format_half_year(window_features.index[-1])},各≥{minimum_window_days}个交易日),两特征')  # 报告样本边界
输入口径:26个完整半年窗口(2013H1—2025H2,各≥60个交易日),两特征

真实数据验证:训练期选择

Listing 2
展开时间切分、训练期标准化与K扫描代码
from sklearn.preprocessing import StandardScaler  # 导入可反变换的标准化器
split_position = int(len(window_features) * 0.70)  # 按时间顺序固定前70%训练、后30%留出
train_features = window_features.iloc[:split_position].copy()  # 仅用早期窗口拟合模型
holdout_features = window_features.iloc[split_position:].copy()  # 隔离后期留出窗口
feature_scaler = StandardScaler().fit(train_features)  # 只在训练期拟合均值与尺度
train_z = pd.DataFrame(feature_scaler.transform(train_features), index=train_features.index, columns=train_features.columns)  # 转换训练特征
silhouette_by_k = {}  # 存放每个K的轮廓系数
for cluster_count in range(2, 6):  # 扫描K=2到5
    fitted_labels = KMeans(n_clusters=cluster_count, n_init=10, random_state=42).fit_predict(train_z)  # 只在训练期固定随机种子拟合
    silhouette_by_k[cluster_count] = silhouette_score(train_z, fitted_labels)  # 记录训练期轮廓系数
best_k = max(silhouette_by_k, key=silhouette_by_k.get)  # 取轮廓系数最高的K
kmeans_model = KMeans(n_clusters=best_k, n_init=10, random_state=42).fit(train_z)  # 用训练期最优K拟合固定质心
train_labels = pd.Series(kmeans_model.labels_, index=train_z.index)  # 保存训练标签
train_distance = kmeans_model.transform(train_z).min(axis=1)  # 计算训练样本到所属质心的距离
distance_reference = float(np.quantile(train_distance, 0.95))  # 以训练距离95%分位数作为固定参照
cluster_share = train_labels.value_counts(normalize=True)  # 训练期各簇占比
silhouette_limit, share_limit = 0.30, 0.15  # 预声明训练分离度与最小簇课堂筛查标准
train_conditions_met = silhouette_by_k[best_k] >= silhouette_limit and cluster_share.min() >= share_limit  # 合并两项训练结果判断
print('各K轮廓系数:' + ','.join(f'K={k}:{v:.3f}' for k, v in silhouette_by_k.items()))  # 展示K扫描结果
print(f'训练截止{format_half_year(train_features.index[-1])}|留出起点{format_half_year(holdout_features.index[0])}|最优K={best_k}|训练轮廓{silhouette_by_k[best_k]:.3f}')  # 展示时间边界与训练分离
print(f'训练最小簇{cluster_share.min():.1%}|训练结果判断={train_conditions_met}')  # 展示训练期检查要求依据
各K轮廓系数:K=2:0.401,K=3:0.411,K=4:0.398,K=5:0.388
训练截止2021H2|留出起点2022H1|最优K=3|训练轮廓0.411
训练最小簇22.2%|训练结果判断=True

真实数据验证:固定留出诊断与判断条件

Listing 3
展开固定质心留出分配、漂移与检查要求代码
holdout_z = pd.DataFrame(feature_scaler.transform(holdout_features), index=holdout_features.index, columns=holdout_features.columns)  # 用训练期固定参数转换留出特征
holdout_labels = pd.Series(kmeans_model.predict(holdout_z), index=holdout_z.index)  # 把留出窗口分配到固定质心
cluster_labels = pd.concat([train_labels, holdout_labels]).sort_index()  # 合并绘图标签但不重新拟合
holdout_distance = kmeans_model.transform(holdout_z).min(axis=1)  # 计算留出样本到所属质心的距离
holdout_within_reference = float((holdout_distance <= distance_reference).mean())  # 计算留出距离覆盖率
feature_drift = holdout_z.mean().sub(train_z.mean()).abs().to_dict()  # 报告标准化特征均值漂移
coverage_limit = 0.80  # 预声明固定质心距离覆盖课堂筛查标准
holdout_conditions_met = holdout_labels.nunique() >= 2 and holdout_within_reference >= coverage_limit  # 合并留出覆盖与状态数门
conditions_met = train_conditions_met and holdout_conditions_met  # 训练或留出任一依据失败即关闭描述状态
decision = '仅作课堂描述' if conditions_met else '暂不采用模型'  # 只给出描述性结果;数据要求未满足时不继续计算
print(f'训练最小簇{cluster_share.min():.1%}|留出距离覆盖{holdout_within_reference:.1%}|特征漂移{feature_drift}')  # 展示留出分配距离与漂移
print(f'检查结果:{decision}|留出覆盖阈值敏感性需比较70%/80%/90%')  # 展示判断结果与敏感性要求
训练最小簇22.2%|留出距离覆盖100.0%|特征漂移{'annualized_return': 0.2620260925578686, 'annualized_volatility': 0.49368560618644153}
检查结果:仅作课堂描述|留出覆盖阈值敏感性需比较70%/80%/90%

真实数据验证:波动状态散点图

展开散点图绘制代码
import matplotlib.pyplot as plt  # 导入绘图库
figure, axis = plt.subplots(figsize=(7.0, 3.8))  # 创建适配幻灯片的横向画布
scatter_handle = axis.scatter(window_features['annualized_volatility'], window_features['annualized_return'], c=cluster_labels, cmap='viridis', s=70, alpha=0.8)  # 窗口散点按簇着色
centers_original = pd.DataFrame(feature_scaler.inverse_transform(kmeans_model.cluster_centers_), columns=window_features.columns)  # 按训练时列顺序把质心反变换回原始尺度
center_volatility = centers_original['annualized_volatility']  # 取原尺度波动率作为横坐标
center_return = centers_original['annualized_return']  # 取原尺度收益率作为纵坐标
axis.scatter(center_volatility, center_return, c='#EC232A', marker='X', s=220, edgecolors='black', linewidths=1.5, label='centroid')  # 红色X标记质心
latest_x = window_features.loc[cluster_labels.index[-1], 'annualized_volatility']  # 最新窗口横坐标
latest_y = window_features.loc[cluster_labels.index[-1], 'annualized_return']  # 最新窗口纵坐标
axis.annotate(format_half_year(cluster_labels.index[-1]), xy=(latest_x, latest_y), xytext=(8, -12), textcoords='offset points', fontsize=16)  # 标注最新窗口
axis.set_xlabel('Annualized volatility')  # 横轴为年化波动率
axis.set_ylabel('Annualized return')  # 纵轴为年化平均收益
axis.grid(True, alpha=0.3)  # 显示浅网格
figure.colorbar(scatter_handle, ax=axis, label='Cluster')  # 添加簇编号颜色条
figure.tight_layout()  # 自动收紧边距
plt.show()  # 显示图形
图中展示沪深300半年窗口K-Means波动状态(真实数据);读者应依据坐标、图例与注释比较主要模式。
Figure 1: 沪深300半年窗口K-Means波动状态(真实数据)

波动状态聚类解读

  • 只有实际输出才能说明训练质心与留出分配;不得预写“高波动且强上涨”等簇含义。
  • 该划分是描述性状态识别:窗口特征由窗口内的已实现收益计算,不能用来说明未来收益方向。
  • 只有通过上一页预声明判断条件后,最新窗口才可作为固定质心的课堂描述;更换指数、窗口长度或数据更新后必须重新拟合训练期并复算留出样本距离要求。

金融领域的聚类应用场景

应用场景 说明
股票分类 根据波动率、收益率等特征分组
客户分层 作为RFM模型之外的补充手段
风险分组 识别高风险投资组合
异常检测 远离所有质心的数据点可能是异常

K-Means 何时不应直接使用?

  • 特征量纲不同而未标准化、离群点主导质心、簇呈月牙形/密度形或规模严重不均衡时,K-Means 的球形等方差近似不合适。
  • 先画散点与距离分布并核对初始化;密度簇可比较 DBSCAN,非球形结构可比较谱聚类,强离群样本先做稳健诊断。
  • 轮廓系数只衡量当前样本可分性;时间稳定性须固定训练期尺度和质心,把后期样本分配到既有质心,再报告质心漂移与分配距离。

本节小结

  • K-Means是一种无监督学习方法,无需标签
  • 用肘部、轮廓系数、稳定性与业务可解释性共同筛选候选 K,不把单一图形写成唯一最优值
  • 核心流程:初始化 → 分配 → 更新 → 收敛
  • 在金融中广泛用于股票分类风险分组
  • 真实数据分析展示完整依据结构:输入口径 → 基线与验证指标 → 预声明判断条件 → 有条件的结论

随堂练习

  • 问题 1|怎样完成本章分析?:依次运行 lst-ch30-local-featureslst-ch30-local-trainlst-ch30-local-check,写出时间分界、训练期标准化参数、K=2—5训练轮廓、训练最小簇占比、留出分配距离覆盖率与特征漂移;再手算一个训练 z 分数。
  • 问题 2|结果说明什么?:说明怎样从原始收益率得到半年特征、怎样按时间划分训练期和验证期、怎样确定聚类中心,以及怎样判断验证期样本与训练期差异过大;请引用三个实际字段或数值说明。
  • 问题 3|换一个情境,怎样继续应用?:将同一时窗特征、训练期标准化与K候选集拓展应用到长三角行业波动分组,写出固定质心下的留出分配距离、漂移、最小簇占比与判断条件状态。
  • 作答提示:请完成三道题,并在回答中引用实际运行结果;拓展练习中不要把课堂示例写成普遍规律或因果结论。

教师参考解答|沪深300波动状态

教师参考解答|答案与说明 1

  • 所用数据与字段index/ 中沪深300复权收盘价;标准列 trade_date, close;半年窗口至少60个交易日;输出 annualized_return, annualized_volatility
  • 完整路线:依次运行本章 lst-ch30-local-featureslst-ch30-local-trainlst-ch30-local-check;教师代码 1 对应原始收益到半年特征,教师代码 2 对应时间切分、训练选择与固定留出判断条件。

教师参考解答|代码 1

展开代码(代码区可独立滚动)
from pathlib import Path  # 导入路径工具以定位规定指数快照
import numpy as np  # 导入数值工具以计算对数收益与年化特征
import pandas as pd  # 导入表格工具以聚合半年窗口
index_path=Path('/home/ubuntu/r2_data_mount/data/index/hs300_index_only.h5')  # 绑定沪深300日行情快照
if not index_path.exists(): raise FileNotFoundError('未找到课程数据文件,请从课程数据下载入口获取并核对文件位置')  # 规定指数资产缺失时终止
hs300_daily=pd.read_hdf(index_path,key='hs300')  # 读取真实指数日行情
trade_date=pd.to_datetime(hs300_daily['datetime'].astype(str).str[:8],format='%Y%m%d')  # 解析八位交易日字符串
daily_log_return=np.log(hs300_daily['close']/hs300_daily['prev_close'])  # 按收盘与昨收计算日对数收益
daily_log_return.index=trade_date  # 将交易日绑定为收益序列索引
window_sample=daily_log_return.sort_index().loc['2013-01-01':'2026-06-30'].dropna()  # 固定课堂半年窗样本期
half_year_code=window_sample.index.year*10+(window_sample.index.month>6).astype(int)+1  # 为每个交易日标记所属半年
window_stats=window_sample.groupby(half_year_code).agg(['count','mean','std'])  # 聚合各半年的观测数、均值与标准差
minimum_window_days=60  # 事先设定完整半年的最小交易日数
window_stats=window_stats.loc[window_stats['count'].ge(minimum_window_days)]  # 删除交易日不足的残缺窗口
window_features=pd.DataFrame({'annualized_return':window_stats['mean']*252,'annualized_volatility':window_stats['std']*np.sqrt(252)})  # 构造半年年化收益与波动特征
if len(window_features)<12: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 前后半窗聚类样本不足时终止

教师参考解答|代码 2

展开代码(代码区可独立滚动)
from sklearn.cluster import KMeans  # 导入K-Means以拟合候选簇数
from sklearn.metrics import silhouette_score  # 导入轮廓系数评价训练期分离度
from sklearn.preprocessing import StandardScaler  # 导入标准化器并只在训练期拟合
split_position=int(len(window_features)*.70)  # 固定前70%训练和后30%留出
train_features=window_features.iloc[:split_position]; holdout_features=window_features.iloc[split_position:]  # 按时间隔离训练与留出特征
feature_scaler=StandardScaler().fit(train_features)  # 仅在训练期拟合尺度参数
train_z=feature_scaler.transform(train_features); holdout_z=feature_scaler.transform(holdout_features)  # 用固定参数转换两段数据
silhouette_scores={cluster_count:silhouette_score(train_z,KMeans(cluster_count,n_init=10,random_state=42).fit_predict(train_z)) for cluster_count in range(2,6)}  # 只用训练期扫描候选K
best_cluster_count=max(silhouette_scores,key=silhouette_scores.get)  # 选择轮廓系数最高的候选簇数
cluster_model=KMeans(best_cluster_count,n_init=10,random_state=42).fit(train_z)  # 在训练期拟合固定质心
train_labels=cluster_model.labels_; holdout_labels=cluster_model.predict(holdout_z)  # 将留出样本分配到固定质心
cluster_shares=pd.Series(train_labels).value_counts(normalize=True)  # 计算训练期最小簇占比
train_distance=cluster_model.transform(train_z).min(axis=1); holdout_distance=cluster_model.transform(holdout_z).min(axis=1)  # 计算所属质心距离
distance_reference=np.quantile(train_distance,.95); holdout_coverage=(holdout_distance<=distance_reference).mean()  # 用训练距离95%分位数核对留出覆盖
feature_drift=np.abs(holdout_z.mean(axis=0)-train_z.mean(axis=0))  # 计算标准化特征均值漂移
adoption_conditions_met=silhouette_scores[best_cluster_count]>=.30 and cluster_shares.min()>=.15 and len(set(holdout_labels))>=2 and holdout_coverage>=.80  # 合并训练分离与留出样本距离要求
decision='仅作课堂描述' if adoption_conditions_met else '暂不采用模型'  # 要求未满足时显式停止,不输出状态标签或配置建议
print({'K':best_cluster_count,'scores':silhouette_scores,'shares':cluster_shares.to_dict(),'holdout_coverage':holdout_coverage,'drift':feature_drift.tolist(),'conditions_met':adoption_conditions_met,'decision':decision})  # 输出训练结果、留出样本比较和是否采用模型

教师参考解答|答案与说明 2

  • 解释答案:标准化使波动率、收益率等不同量纲特征能共同参与欧氏距离;标准化器、K与质心只在早期训练窗拟合,后期留出窗只做固定分配。分配距离和特征漂移是时间外诊断,聚类标签本身仍不是涨跌预测。
  • 拓展应用答案:拓展应用到长三角行业波动分组时,替换证券池与同一时间段的收益特征,保持时间分界、训练期标准化和 K 候选集;用行业留出窗的分配距离覆盖率与漂移核对。
  • 参考结果:时间分界、训练期不同 K 值的比较、各组占比、留出样本距离覆盖率和特征变化,以及是否符合采用条件;结果只能是“仅作课堂描述”或“暂不采用模型”。另行核对:比较 70%/80%/90% 三种覆盖率要求,并确认留出期未参与模型拟合。
  • 常见错误:聚类不预测未来;不得全样本拟合、用价格水平聚类、调K后只报最好结果或把簇编号当风险等级。